/*
 ------------------------------------------------------------------------------
 Copyright (C) 2013 Eternal Games.

 This file is part of the GLQuake source code.

 The GLQuake source code is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or (at your
 option) any later version.

 The GLQuake source code is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 more details.

 You should have received a copy of the GNU General Public License along with
 the GLQuake source code; if not, write to the Free Software Foundation, Inc.,
 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 ------------------------------------------------------------------------------
*/


//
// physics_player.cpp - player movement physics
//


#include "../game.h"


#define	PM_OVERCLIP						1.001f

#define PM_MIN_WALK_NORMAL				0.7f	// Can't walk on very steep slopes

// Constant movement parameters
#define PM_FRICTION						6.0f
#define PM_SLIDE_FRICTION				3.0f
#define PM_FLY_FRICTION					5.0f
#define PM_NOCLIP_FRICTION				10.0f
#define PM_WATER_FRICTION				1.0f
#define PM_ACID_FRICTION				1.0f
#define PM_LAVA_FRICTION				4.0f

#define PM_ACCELERATE					10.0f
#define PM_AIR_ACCELERATE				1.0f
#define PM_SLIDE_ACCELERATE				20.0f
#define PM_FLY_ACCELERATE				8.0f
#define PM_NOCLIP_ACCELERATE			10.0f

#define PM_STOP_SPEED					100.0f

#define PM_SWIM_SCALE					0.5f


/*
 ==============================================================================

 TODO

 ==============================================================================
*/


/*
 ==================
 etPhysicsPlayer::AddEvent
 ==================
*/
void etPhysicsPlayer::AddEvent (int eventType, int eventParm){

	int		index;

	index = state->eventSequence & (MAX_EVENTS - 1);

	state->eventSequence++;
	state->eventTypes[index] = eventType;
	state->eventParms[index] = eventParm;
}


/*
 ==============================================================================

 TODO

 ==============================================================================
*/


/*
 ==================
 etPhysicsPlayer::Friction

 Handles both ground friction and water friction
 ==================
*/
void etPhysicsPlayer::Friction (){

	glqVec3	vel;
	float	speed, stopSpeed, newSpeed;
	float	drop, control, friction;

	vel = state->velocity;

	if (walking)
		vel.z = 0.0f;		// Ignore slope movement

	speed = vel.Length();
	if (speed < 1.0f){
		if (state->pmType == PM_NOCLIP || state->pmType == PM_SPECTATOR){
			state->velocity.Clear();
			return;
		}

		// Allow sinking underwater
		state->velocity.x = 0.0f;
		state->velocity.y = 0.0f;

		return;
	}

	stopSpeed = playerSpeed * 0.5f;
	if (stopSpeed > PM_STOP_SPEED)
		stopSpeed = PM_STOP_SPEED;

	drop = 0.0f;

	// Apply ground friction
	if (walking && waterLevel <= WATERLEVEL_WAIST){
		// If getting knocked back, no friction
		if (!(state->pmFlags & PMF_TIME_KNOCKBACK)){
			control = speed;
			if (control < stopSpeed)
				control = stopSpeed;

			if (!(state->pmFlags & PMF_SLIDING))
				friction = PM_FRICTION;
			else {
				friction = PM_SLIDE_FRICTION;

				// Reduced or extra friction on slopes
				if (state->velocity.z < 0.0f)
					friction *= (groundTrace.c.plane.Normal().z - PM_MIN_WALK_NORMAL) / (1.0f - PM_MIN_WALK_NORMAL);
				else if (state->velocity.z > 0.0f)
					friction += PM_SLIDE_FRICTION * (1.0f - (groundTrace.c.plane.Normal().z - PM_MIN_WALK_NORMAL) / (1.0f - PM_MIN_WALK_NORMAL));

				// Allow players to apply extra friction
				if (userCmd.upMove > 0)
					friction *= 10.0f;
			}

			drop += control * friction * frameTime;
		}
	}

	// Apply noclip friction
	if (state->pmType == PM_NOCLIP){
		control = speed;
		if (control < stopSpeed)
			control = stopSpeed;

		drop += control * PM_NOCLIP_FRICTION * frameTime;
	}

	// Apply flying friction
	if (state->pmType == PM_SPECTATOR)
		drop += speed * PM_FLY_FRICTION * frameTime;

	// Apply water friction even if just wading
	if (waterLevel != WATERLEVEL_NONE){
		if (waterType & CONTENT_LAVA)
			drop += speed * PM_LAVA_FRICTION * waterLevel * frameTime;
		else if (waterType & CONTENT_ACID)
			drop += speed * PM_ACID_FRICTION * waterLevel * frameTime;
		else
			drop += speed * PM_WATER_FRICTION * waterLevel * frameTime;
	}

	// Scale the velocity
	newSpeed = speed - drop;
	if (newSpeed < 0.0f)
		newSpeed = 0.0f;

	state->velocity *= (newSpeed / speed);
}

/*
 ==================
 etPhysicsPlayer::Accelerate

 Handles user intended acceleration
 ==================
*/
void etPhysicsPlayer::Accelerate (const glqVec3 &wishDir, float wishSpeed, float accel){

	float	addSpeed, accelSpeed;

	addSpeed = wishSpeed - state->velocity.Dot(wishDir);
	if (addSpeed <= 0.0f)
		return;

	accelSpeed = accel * wishSpeed * frameTime;
	if (accelSpeed > addSpeed)
		accelSpeed = addSpeed;

	state->velocity += wishDir * accelSpeed;
}

/*
 ==================
 etPhysicsPlayer::UserCmdScale

 Returns the scale factor to apply to movements.
 This allows the clients to use axial -127 to 127 values for all directions
 without getting a sqrt(2) distortion in speed.
 ==================
*/
float etPhysicsPlayer::UserCmdScale () const {

	float	total, scale;
	int		forwardMove, rightMove, upMove;
	int		max;

	if (state->pmType == PM_DEAD)
		return 0.0f;

	if (state->pmType == PM_NOCLIP || state->pmType == PM_SPECTATOR){
		forwardMove = glqMath::Abs(userCmd.forwardMove);
		rightMove = glqMath::Abs(userCmd.sideMove);
		upMove = glqMath::Abs(userCmd.upMove);
	}
	else {
		forwardMove = glqMath::Abs(userCmd.forwardMove);
		rightMove = glqMath::Abs(userCmd.sideMove);

		if (waterLevel <= WATERLEVEL_WAIST)
			upMove = 0;
		else
			upMove = glqMath::Abs(userCmd.upMove);
	}

	max = forwardMove;
	if (rightMove > max)
		max = rightMove;
	if (upMove > max)
		max = upMove;

	if (!max)
		return 0.0f;

	total = glqMath::Sqrt(forwardMove * forwardMove + rightMove * rightMove + upMove * upMove);

	scale = playerSpeed * max / (127.0f * total);

	return scale;
}


/*
 ==============================================================================

 TODO

 ==============================================================================
*/


/*
 ==================
 etPhysicsPlayer::UpdateViewAngles
 ==================
*/
void etPhysicsPlayer::UpdateViewAngles (){

	glqAngles	viewAngles;

	if (state->pmType == PM_DEAD)
		return;		// No view changes at all

	// Save current view angles
	viewAngles = state->viewAngles;

	// Circularly clamp the angles with deltas
	state->viewAngles[0] = SHORT2ANGLE(userCmd.angles[0]) + state->deltaAngles[0];
	state->viewAngles[1] = SHORT2ANGLE(userCmd.angles[1]) + state->deltaAngles[1];
	state->viewAngles[2] = SHORT2ANGLE(userCmd.angles[2]) + state->deltaAngles[2];

	state->viewAngles.Normalize180();

	// Don't let the player look up or down more than X degrees
	if (state->pmType == PM_NOCLIP || state->pmType == PM_SPECTATOR)
		state->viewAngles.pitch = glqMath::ClampFloat(state->viewAngles.pitch, -89.0f, 89.0f);
	else
		state->viewAngles.pitch = glqMath::ClampFloat(state->viewAngles.pitch, pm_minNormalPitch->floatValue, pm_maxNormalPitch->floatValue);

	// Update the deltas
	state->deltaAngles[0] = state->viewAngles[0] - SHORT2ANGLE(userCmd.angles[0]);
	state->deltaAngles[1] = state->viewAngles[1] - SHORT2ANGLE(userCmd.angles[1]);
	state->deltaAngles[2] = state->viewAngles[2] - SHORT2ANGLE(userCmd.angles[2]);
}


/*
 ==============================================================================

 TODO

 ==============================================================================
*/


/*
 ==================

 ==================
*/
void etPhysicsPlayer::SetWaterLevel (){

}

/*
 ==================

 ==================
*/
void etPhysicsPlayer::SetMoveSpeed (){

	if (state->pmType == PM_DEAD)
		return;

	if (state->pmType == PM_NOCLIP){
		playerSpeed = pm_noclipSpeed->floatValue;
		return;
	}

	if (state->pmType == PM_SPECTATOR){
		playerSpeed = pm_spectateSpeed->floatValue;
		return;
	}

	if (userCmd.flags & UCF_RUNNING)
		playerSpeed = pm_runSpeed->floatValue;
	else
		playerSpeed = pm_walkSpeed->floatValue;

	// TODO: slow down the player if we're moving in water
}

/*
 ==================

 ==================
*/
void etPhysicsPlayer::DropTimers (){

	// Drop misc timer
	if (state->pmTime){
		state->pmTime -= frameMsec;

		if (state->pmTime <= 0){
			state->pmFlags &= ~PMF_ALL_TIMES;
			state->pmTime = 0;
		}
	}
}


/*
 ==============================================================================

 TODO

 ==============================================================================
*/


/*
 ==================
 etPhysicsPlayer::CheckStance

 Sets bounds and view height
 ==================
*/
void etPhysicsPlayer::CheckStance (){

	// Special case for dead
	if (state->pmType == PM_DEAD){
		playerBounds[0].Set(-pm_boxWidth->floatValue * 0.5f, -pm_boxWidth->floatValue * 0.5f, 0.0f);
		playerBounds[1].Set( pm_boxWidth->floatValue * 0.5f,  pm_boxWidth->floatValue * 0.5f, pm_deadHeight->floatValue);

		state->bounds = G_EncodeBounds(playerBounds);
		state->viewHeight = pm_deadViewHeight->floatValue;

		return;
	}

	// Special case for noclip
	if (state->pmType == PM_NOCLIP){
		playerBounds[0].Set(-pm_boxWidth->floatValue * 0.5f, -pm_boxWidth->floatValue * 0.5f, 0.0f);
		playerBounds[1].Set( pm_boxWidth->floatValue * 0.5f,  pm_boxWidth->floatValue * 0.5f, pm_normalHeight->floatValue);

		state->bounds = G_EncodeBounds(playerBounds);
		state->viewHeight = pm_normalViewHeight->floatValue;

		return;
	}

	// Special case for spectator
	if (state->pmType == PM_SPECTATOR){
		playerBounds[0].Set(-pm_spectateBoxSize->floatValue * 0.5f, -pm_spectateBoxSize->floatValue * 0.5f, 0.0f);
		playerBounds[1].Set( pm_spectateBoxSize->floatValue * 0.5f,  pm_spectateBoxSize->floatValue * 0.5f, pm_spectateBoxSize->floatValue);

		state->bounds = G_EncodeBounds(playerBounds);
		state->viewHeight = pm_spectateBoxSize->floatValue * 0.5f;

		return;
	}

	// Set bounds and view height
	playerBounds[0].Set(-pm_boxWidth->floatValue * 0.5f, -pm_boxWidth->floatValue * 0.5f, 0.0f);
	playerBounds[1].Set( pm_boxWidth->floatValue * 0.5f,  pm_boxWidth->floatValue * 0.5f, pm_normalHeight->floatValue);

	state->bounds = G_EncodeBounds(playerBounds);
	state->viewHeight = pm_normalViewHeight->floatValue;
}

/*
 ==================

 Checks the ground entity
 ==================
*/
void etPhysicsPlayer::CheckGround (){

	// TODO!!!
}

/*
 ==================
 etPhysicsPlayer::CheckJump
 ==================
*/
bool etPhysicsPlayer::CheckJump (){

	if (state->pmTime)
		return false;

	if (userCmd.upMove < 10)
		return false;		// Not holding jump

	// Must wait for jump to be released
	if (state->pmFlags & PMF_JUMP_HELD)
		return false;

	// Jumping away
	walking = false;
	groundPlane = false;

	state->pmFlags |= PMF_JUMP_HELD;
	state->groundEntityNum = ENTITYNUM_NONE;

	state->velocity.z += glqMath::Sqrt(2.0f * pm_jumpHeight->floatValue * gravity);

	// Add an event
	AddEvent(EV_JUMP, 0);

	return true;
}

/*
 ==================

 ==================
*/
bool etPhysicsPlayer::CheckWaterJump (){

	if (state->pmTime)
		return false;

	if (userCmd.forwardMove <= 0 || userCmd.upMove <= 0)
		return false;

	// Check for water jump
	if (waterLevel < WATERLEVEL_KNEES || waterLevel > WATERLEVEL_SHOULDERS)
		return false;

	// TODO!!!

	return true;
}


/*
 ==============================================================================

 MOVEMENT TYPES

 ==============================================================================
*/


/*
 ==================
 
 Returns true if the velocity was clipped in some way
 ==================
*/
bool etPhysicsPlayer::SlideMove (bool addGravity){

	return false;
}

/*
 ==================

 ==================
*/
void etPhysicsPlayer::StepSlideMove (bool addGravity){

}

/*
 ==================
 etPhysicsPlayer::NoClipMove
 ==================
*/
void etPhysicsPlayer::NoClipMove (){

	glqVec3	wishDir;
	float	wishSpeed;
	float	scale;

	// Apply friction
	Friction();

	// User intentions
	scale = UserCmdScale();

	wishDir = viewForward * userCmd.forwardMove + viewRight * userCmd.sideMove;
	wishDir.z += userCmd.upMove;

	wishSpeed = wishDir.Normalize() * scale;

	Accelerate(wishDir, wishSpeed, PM_NOCLIP_ACCELERATE);

	// Move
	state->origin += state->velocity * frameTime;
}

/*
 ==================
 etPhysicsPlayer::SpectatorMove
 ==================
*/
void etPhysicsPlayer::SpectatorMove (){

	glqVec3	wishDir;
	float	wishSpeed;
	float	scale;

	// Apply friction
	Friction();

	// User intentions
	scale = UserCmdScale();

	wishDir = viewForward * userCmd.forwardMove + viewRight * userCmd.sideMove;
	wishDir.z += userCmd.upMove;

	wishSpeed = wishDir.Normalize() * scale;

	Accelerate(wishDir, wishSpeed, PM_FLY_ACCELERATE);

	// Move
	SlideMove(false);
}

/*
 ==================
 etPhysicsPlayer::WaterJumpMove
 ==================
*/
void etPhysicsPlayer::WaterJumpMove (){

	// Water jump has no control
	SlideMove(false);

	// Remove up velocity as soon as we get onto the surface
	if (state->pmTime < 250)
		state->velocity.z = 0.0f;
}

/*
 ==================

 ==================
*/
void etPhysicsPlayer::WaterMove (){

}

/*
 ==================
 etPhysicsPlayer::AirMove
 ==================
*/
void etPhysicsPlayer::AirMove (){

	glqVec3	wishDir;
	float	wishSpeed;
	float	scale;

	Friction();

	scale = UserCmdScale();

	// Project moves down to flat plane
	viewForward.z = 0.0f;
	viewRight.z = 0.0f;

	viewForward.Normalize();
	viewRight.Normalize();

	wishDir = viewForward * userCmd.forwardMove + viewRight * userCmd.sideMove;

	wishSpeed = wishDir.Normalize() * scale;

	// Clamp the speed lower if falling into water
	if (waterLevel != WATERLEVEL_NONE){
		scale = 1.0f - (1.0f - PM_SWIM_SCALE) * (waterLevel / 5.0f);

		if (wishSpeed > playerSpeed * scale)
			wishSpeed = playerSpeed * scale;
	}

	// Not on ground, so little effect on velocity
	Accelerate(wishDir, wishSpeed, PM_AIR_ACCELERATE);

	// We may have a ground plane that is very steep, even though we don't have
	// a ground entity.
	// Slide along the steep plane.
	if (groundPlane)
		state->velocity.ProjectOntoPlane(state->velocity, groundTrace.c.plane.Normal(), PM_OVERCLIP);

	SlideMove(true);
}

/*
 ==================
 etPhysicsPlayer::WalkMove
 ==================
*/
void etPhysicsPlayer::WalkMove (){

	glqVec3	wishDir;
	float	wishSpeed;
	glqVec3	oldVelocity;
	float	scale;

	if (waterLevel > WATERLEVEL_FEET){
		if (CheckWaterJump()){
			// Jumped out of water
			WaterJumpMove();
			return;
		}
	}

	if (CheckJump()){
		// Jumped away
		AirMove();
		return;
	}

	Friction();

	scale = UserCmdScale();

	// Project moves down to flat plane
	viewForward.z = 0.0f;
	viewRight.z = 0.0f;

	// Project the forward and right directions onto the ground plane
	viewForward.ProjectOntoPlane(viewForward, groundTrace.c.plane.Normal(), PM_OVERCLIP);
	viewRight.ProjectOntoPlane(viewRight, groundTrace.c.plane.Normal(), PM_OVERCLIP);

	viewForward.Normalize();
	viewRight.Normalize();

	wishDir = viewForward * userCmd.forwardMove + viewRight * userCmd.sideMove;

	wishSpeed = wishDir.Normalize() * scale;

	// Clamp the speed lower if wading
	if (waterLevel != WATERLEVEL_NONE){
		scale = 1.0f - (1.0f - PM_SWIM_SCALE) * (waterLevel / 5.0f);

		if (wishSpeed > playerSpeed * scale)
			wishSpeed = playerSpeed * scale;
	}

	// When a player gets hit, they temporarily lose full control, which allows
	// them to be moved a bit
	if ((state->pmFlags & PMF_TIME_KNOCKBACK)){
		Accelerate(wishDir, wishSpeed, PM_AIR_ACCELERATE);

		state->velocity.z -= gravity * frameTime;
	}
	else {
		if (!(state->pmFlags & PMF_SLIDING))
			Accelerate(wishDir, wishSpeed, PM_ACCELERATE);
		else 
			Accelerate(wishDir, wishSpeed, PM_SLIDE_ACCELERATE);
	}

	scale = state->velocity.Length();
	oldVelocity = state->velocity;

	// Slide along the ground plane
	state->velocity.ProjectOntoPlane(state->velocity, groundTrace.c.plane.Normal(), PM_OVERCLIP);

	// Only maintain speed if the direction is similar
	if (state->velocity.Dot(oldVelocity) > 0.0f){
		// Don't decrease velocity when going up or down a slope
		state->velocity.Normalize();
		state->velocity *= scale;
	}

	// Don't do anything if standing still
	if (!state->velocity.x && !state->velocity.y)
		return;

	StepSlideMove(false);
}


/*
 ==============================================================================

 TODO

 ==============================================================================
*/


/*
 ==================
 
 ==================
*/
void etPhysicsPlayer::MoveSingle (int msec){

	// Clear results
	waterType = 0;
	waterLevel = WATERLEVEL_NONE;

	// Clear all the flags if not doing normal movement
	if (state->pmType != PM_NORMAL)
		state->pmFlags &= ~PMF_ALL_FLAGS;

	if (state->pmType == PM_FREEZE)
		return;		// No movement at all

	// Determine the time
	frameMsec = msec;
	frameTime = MS2SEC(msec);

	// Clear local variables
	playerSpeed = 0.0f;
	playerBounds.Zero();

	walking = false;
	groundPlane = false;

	// Update the view angles
	UpdateViewAngles();

	state->viewAngles.ToVectors(&viewForward, &viewRight, NULL);

	// Special case for noclip
	if (state->pmType == PM_NOCLIP){
		CheckStance();
		SetMoveSpeed();
		DropTimers();
		NoClipMove();

		return;
	}

	// Special case for spectator
	if (state->pmType == PM_SPECTATOR){
		CheckStance();
		SetMoveSpeed();
		DropTimers();
		SpectatorMove();

		return;
	}

	// Clear the jump held flag if needed
	if (userCmd.upMove < 10)
		state->pmFlags &= ~PMF_JUMP_HELD;

	// Set ground entity
	CheckGround();

	// Set bounds and view height
	CheckStance();

	// Set water level
	SetWaterLevel();

	// Set movement speed
	SetMoveSpeed();

	// Update timers
	DropTimers();

	// No control at all when dead
	if (state->pmType == PM_DEAD){
		userCmd.forwardMove = 0;
		userCmd.sideMove = 0;
		userCmd.upMove = 0;
	}

	// Move
	if (waterLevel > WATERLEVEL_WAIST)
		WaterMove();
	else if (walking)
		WalkMove();
	else
		AirMove();

	// Set ground entity
	CheckGround();

	// Set water level
	SetWaterLevel();
}

/*
 ==================
 etPhysicsPlayer::MovePlayer

 Can be called by either the client or the server
 ==================
*/
void etPhysicsPlayer::MovePlayer (){

	int		finalTime;
	int		msec;

	finalTime = ((userCmd.time + pm_msec->integerValue - 1) / pm_msec->integerValue) * pm_msec->integerValue;

	if (finalTime <= state->userCmdTime)
		return;		// Should not happen

	if (finalTime > state->userCmdTime + 1000)
		state->userCmdTime = finalTime - 1000;

	// Chop the move up if it is too long, to prevent framerate dependent
	// behaviour
	while (state->userCmdTime < finalTime){
		msec = finalTime - state->userCmdTime;
		if (msec > pm_msec->integerValue)
			msec = pm_msec->integerValue;

		MoveSingle(msec);

		state->userCmdTime += msec;
	}
}